﻿using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
using TallyJ1Announcer.VoterRelated;
using Application = System.Windows.Application;
using Cursors = System.Windows.Input.Cursors;
using KeyEventArgs = System.Windows.Input.KeyEventArgs;
using MessageBox = System.Windows.MessageBox;
using MouseEventArgs = System.Windows.Input.MouseEventArgs;

namespace TallyJ1Announcer
{
	/// <summary>
	/// Notes
	/// </summary>
	/// <remarks>
	/// 
	/// Key strokes: 
	///    Down/Space/PageDown - move to next name
	///    Up/PageUp - move to previous
	///    B - blank/show display
	/// 
	/// Xml file to load: defined in Settings file
	/// 
	/// 
	/// 
	/// </remarks>
	public partial class MainWindow
	{
		const int TopPadding = 6;
		const int BottomPadding = 5;
		int _currentScreenNum;
		int _currentlySelectedIndex;
		DateTime _lastMouseWheelEvent = DateTime.MinValue;
		int _numberOfLinesToMove;
		bool _prepared;
		ViewState _currentScreenState;
		Point _currentMousePosition;

		public MainWindow()
		{
			InitializeComponent();

			SetScreenToState(ViewState.BeforeStarting);

			VoterSource = new VoterSource(TopPadding, BottomPadding);

			if (!VoterSource.IsReady)
			{
				MessageBox.Show(string.Join("\n", VoterSource.ErrorMessages.Joined("\n")),
								Properties.Resources.ApplicationTitle, MessageBoxButton.OK, MessageBoxImage.Error);


				Application.Current.Shutdown();
				return;
			}

			DataContext = VoterSource;
			//mainListBox.DataContext = VoterSource;
			mainListBox.Focus();

			VoterSource.NamesReloaded += NameSourceOnNamesReloaded;

			mainListBox.PreviewKeyDown += MainListBoxOnPreviewKeyDown;
			PreviewKeyDown += OnPreviewKeyDown;

			mainListBox.PreviewMouseWheel += MainListBoxOnMouseWheel;
			mainListBox.PreviewMouseDown += MainListBoxOnMouseDown;


			mainListBox.GotFocus += (o, eventArgs) => eventArgs.Handled = true;

			GotFocus += (sender, args) =>
							{
								if (sender is Window)
								{
									mainListBox.Focus();
								}
							};

			Title = Properties.Resources.ApplicationTitle;


		}

		protected VoterSource VoterSource
		{
			get;
			set;
		}

		protected override void OnContentRendered(EventArgs e)
		{
			base.OnContentRendered(e);
			mainListBox.RenderTransform.Transform(new Point(0, 0 - 0.2 * mainListHolder.ActualHeight));
			mainListBox.Height = mainListHolder.ActualHeight * 1.4;
		}


		void NameSourceOnNamesReloaded(object sender, EventArgs eventArgs)
		{
			mainListBox.DataContext = VoterSource; // reload??

			if (_currentScreenState == ViewState.ShowingNames)
			{
				Move(MoveDirection.NoChange);
			}
		}

		void PrepareScreen()
		{
			_prepared = true;

			// show first names
			Move(MoveDirection.NextDown, 5);
		}

		void MainListBoxOnMouseDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
		{
			switch (mouseButtonEventArgs.ChangedButton)
			{
				case MouseButton.Left:
					Move(MoveDirection.NextDown);
					mouseButtonEventArgs.Handled = true;
					break;

				case MouseButton.Middle:
					break;

				case MouseButton.Right:
					Move(MoveDirection.PrevUp);
					mouseButtonEventArgs.Handled = true;
					break;

				case MouseButton.XButton1:
					break;
				case MouseButton.XButton2:
					break;
				default:
					throw new ArgumentOutOfRangeException();
			}
		}

		void MainListBoxOnMouseWheel(object sender, MouseWheelEventArgs mouseWheelEventArgs)
		{
			mouseWheelEventArgs.Handled = true;

			// don't care about how big the delta is... on each move of the wheel, just move one 1 line

			var delta = mouseWheelEventArgs.Delta;
			if (delta == 0)
			{
				return;
			}

			if (DateTime.Now - _lastMouseWheelEvent < TimeSpan.FromSeconds(1))
			{
				return;
			}

			_lastMouseWheelEvent = DateTime.Now;

			var direction = delta < 0 ? MoveDirection.NextDown : MoveDirection.PrevUp;

			Move(direction);
		}

		void Move(MoveDirection direction, int numberOfLines = 1)
		{
			if (!_prepared)
			{
				PrepareScreen();
				return;
			}

			if (_currentScreenState != ViewState.ShowingNames)
			{
				SetScreenToState(ViewState.ShowingNames);
				return;
			}

			if (numberOfLines == 1 && _numberOfLinesToMove > 0)
			{
				// must be in fast forward, but pressed some other key... just stop here
				_numberOfLinesToMove = 0;
				return;
			}

			if (direction == MoveDirection.NoChange)
			{
				return;
			}

			_numberOfLinesToMove = numberOfLines;

			var sv = FindScrollViewer(mainListBox);
			if (sv == null)
			{
				return;
			}

			if (!MoveVirtual(direction))
			{
				_numberOfLinesToMove = 0;
				return;
			}

			HideCursor();

			AnimatedMove(sv, direction);
		}

		void AnimatedMove(ScrollViewer sv, MoveDirection direction)
		{
			var lineSize = mainListHolder.ActualHeight / 5.0;

			const double fastSpeed = .1;
			const double normalSpeed = 1.25;

			Thickness offsetMargin;
			var normalMargins = new Thickness(0, -lineSize, 0, -lineSize);

			var speed = TimeSpan.FromSeconds(_numberOfLinesToMove > 1 ? fastSpeed : normalSpeed); // faster if fast-forwarding

			if (direction == MoveDirection.NextDown)
			{
				// moving to next line, sliding up

				offsetMargin = new Thickness(0, 0, 0, -2 * lineSize);

				sv.LineDown();
				mainListBox.Margin = offsetMargin;

				var animation = new ThicknessAnimation
									{
										From = offsetMargin,
										To = normalMargins,
										Duration = new Duration(speed),
										AutoReverse = false,
										EasingFunction = new ExponentialEase
															{
																EasingMode = EasingMode.EaseInOut,
																Exponent = 3
															}
									};

				animation.Completed += (sender, args) =>
										{
											_numberOfLinesToMove--;


											if (_numberOfLinesToMove > 0)
											{
												Dispatcher.BeginInvoke(DispatcherPriority.Input, (Action<MoveDirection, int>)Move,
																	   direction, _numberOfLinesToMove);
											}
										};

				mainListBox.BeginAnimation(MarginProperty, animation, HandoffBehavior.SnapshotAndReplace);
			}
			else
			{
				// going up

				offsetMargin = new Thickness(0, -2 * lineSize, 0, 0);

				mainListBox.Margin = offsetMargin;
				sv.LineUp();

				var animation = new ThicknessAnimation
									{
										From = offsetMargin,
										To = normalMargins,
										Duration = new Duration(speed),
										AutoReverse = false,
										EasingFunction = new ExponentialEase
															{
																EasingMode = EasingMode.EaseInOut,
																Exponent = 3
															}
									};

				animation.Completed += (sender, args) =>
										{
											_numberOfLinesToMove--;

											if (_numberOfLinesToMove > 0)
											{
												Dispatcher.BeginInvoke(DispatcherPriority.Input, (Action<MoveDirection, int>)Move,
																	   direction, _numberOfLinesToMove);
											}
										};

				mainListBox.BeginAnimation(MarginProperty, animation,
										   HandoffBehavior.SnapshotAndReplace);
			}
		}

		bool MoveVirtual(MoveDirection direction)
		{
			var delta = (int)direction;

			var max = mainListBox.Items.Count - 3;

			//var current = mainListBox.SelectedIndex;
			var current = _currentlySelectedIndex;
			var next = current + delta;

			var virtualMax = max - 1;
			const int virtualMin = 0;

			if (direction == MoveDirection.NextDown)
			{
				VoterSource.Viewing(next);

				if (next >= virtualMax)
				{
					_numberOfLinesToMove = 0;
					return false;
				}
			}
			if (direction == MoveDirection.PrevUp && next < virtualMin)
			{
				_numberOfLinesToMove = 0;
				return false;
			}

			mainListBox.SelectedIndex = next;
			_currentlySelectedIndex = next;

			return true;
		}

		void MainListBoxOnPreviewKeyDown(object sender, KeyEventArgs keyEventArgs)
		{
			HandleKeys(keyEventArgs);
		}

		void HandleKeys(KeyEventArgs keyEvent)
		{
			if (_currentScreenState == ViewState.ShowingNames)
			{
				switch (keyEvent.Key)
				{
					case Key.F:
						if (_currentScreenState == ViewState.ShowingNames)
						{
							ToggleFastForward();
						}
						break;

					case Key.Down:
					case Key.Right:
					case Key.Space:
						Move(MoveDirection.NextDown);
						keyEvent.Handled = true;
						break;

					case Key.PageDown:
						Move(MoveDirection.NextDown, 5);
						keyEvent.Handled = true;
						break;

					case Key.Left:
					case Key.Up:
						Move(MoveDirection.PrevUp);
						keyEvent.Handled = true;
						break;

					case Key.PageUp:
						Move(MoveDirection.PrevUp, 5);
						keyEvent.Handled = true;
						break;

					case Key.Home:
						Move(MoveDirection.PrevUp, 9999);
						keyEvent.Handled = true;
						break;

					case Key.End:
						Move(MoveDirection.NextDown, 9999);
						keyEvent.Handled = true;
						break;


					//case Key.Left:
					//    var current = mainListBox.Margin;
					//    var ani = new ThicknessAnimation(current,
					//                                     new Thickness(current.Left, current.Top + 50, current.Right, current.Bottom - 50),
					//                                     TimeSpan.FromSeconds(.5));
					//    mainListBox.BeginAnimation(MarginProperty, ani);
					//    keyEventArgs.Handled = true;
					//    break;

					//case Key.Right:
					//    current = mainListBox.Margin;
					//    ani = new ThicknessAnimation(current,
					//                                 new Thickness(current.Left, current.Top - 50, current.Right, current.Bottom + 50),
					//                                 TimeSpan.FromSeconds(.5));
					//    mainListBox.BeginAnimation(MarginProperty, ani);
					//    keyEventArgs.Handled = true;
					//    break;

				}
			}

			switch (keyEvent.Key)
			{
				case Key.Escape:
					keyEvent.Handled = true;

					if (_currentScreenState == ViewState.BlankedWithMessage)
					{
						Close();
						return;
					}

					SetScreenToState(ViewState.BlankedWithMessage);
					break;

				case Key.F1:
					ShowHelp();
					break;

				case Key.F2:
					DoKeyF2();
					break;


				case Key.B:
					SetScreenToState(ViewState.BlankedWithMessage);
					keyEvent.Handled = true;
					break;

				case Key.P:
					SetScreenToState(ViewState.ShowingPercent);
					keyEvent.Handled = true;
					break;

				case Key.N:
					SetScreenToState(ViewState.ShowingNames);
					keyEvent.Handled = true;
					break;

				default:
					// avoid any wrong keys	
					keyEvent.Handled = true;
					break;
			}

			switch (keyEvent.SystemKey)
			{
				case Key.Right:
					if (IsAltPressed())
					{
						SwitchScreen(keyEvent, 1);
					}

					break;

				case Key.Left:
					if (IsAltPressed())
					{
						SwitchScreen(keyEvent, -1);
					}
					break;
			}
		}

		void DoKeyF2()
		{
			VoterSource.AskForNewSourceFile();
			var oldState = _currentScreenState;

			SetScreenToState(ViewState.BlankedWithMessage);

			mainListBox.SelectedIndex = 0;
			mainListBox.ScrollIntoView(mainListBox.Items[0]);
			_currentlySelectedIndex = 0;
			_prepared = false;

			SetScreenToState(oldState);
		}

		void ToggleFastForward()
		{
			var newLines = _numberOfLinesToMove > 0 ? 0 : 9999;

			if (newLines > 0)
			{
				Move(MoveDirection.NextDown, newLines);
			}
			else
			{
				_numberOfLinesToMove = 0;
			}
		}

		void ShowHelp()
		{
			MessageBox.Show(string.Format(Properties.Resources.QuickHelpText, VoterSource.SourceFileName),
							Properties.Resources.ApplicationTitle);
		}

		/// <summary>
		/// Determine which screen to display on. If there is only one, it refreshes the display to 
		/// match the size of the screen.
		/// </summary>
		/// <param name="routedEventArgs"></param>
		/// <param name="i"></param>
		void SwitchScreen(RoutedEventArgs routedEventArgs, int i)
		{
			var screens = Screen.AllScreens;
			if (screens.Length == 1)
				return;

			var newScreen = _currentScreenNum + i;
			if (newScreen > screens.Length - 1)
			{
				newScreen = 0;
			}
			else if (newScreen < 0)
			{
				newScreen = screens.Length - 1;
			}

			_currentScreenNum = newScreen;

			ShowOnScreen(screens[newScreen]);

			routedEventArgs.Handled = true;
		}

		/// <summary>
		/// Fit our display to this screen
		/// </summary>
		/// <param name="screen"></param>
		void ShowOnScreen(Screen screen)
		{
			var bounds = screen.Bounds;

			Hide();
			WindowState = WindowState.Normal;

			Top = bounds.Top;
			Left = bounds.Left;
			Width = bounds.Width;
			Height = bounds.Height;

			WindowState = WindowState.Maximized;
			Show();
		}

		static bool IsAltPressed()
		{
			return (Keyboard.GetKeyStates(Key.LeftAlt) & KeyStates.Down) > 0
				   || (Keyboard.GetKeyStates(Key.RightAlt) & KeyStates.Down) > 0;
		}

		void OnPreviewKeyDown(object sender, KeyEventArgs e)
		{
			HandleKeys(e);
		}

		enum ViewState
		{
			BeforeStarting,
			BlankedWithMessage,
			ShowingNames,
			ShowingPercent
		}

		void SetScreenToState(ViewState newState)
		{
			_currentScreenState = newState;
			switch (newState)
			{
				case ViewState.BeforeStarting:
					OnScreenHint.Text = "Ready to Begin. Press N for names, P for percentage, F1 for more...";

					mainGrid.Visibility = Visibility.Hidden;
					PercentDisplay.Visibility = Visibility.Hidden;
					OnScreenHint.Visibility = Visibility.Visible;
					break;

				case ViewState.BlankedWithMessage:
					OnScreenHint.Text = "To resume, press N for names, P for percentage, or Esc to close and exit.";

					mainGrid.Visibility = Visibility.Hidden;
					PercentDisplay.Visibility = Visibility.Hidden;
					OnScreenHint.Visibility = Visibility.Visible;
					break;

				case ViewState.ShowingNames:
					mainGrid.Visibility = Visibility.Visible;
					PercentDisplay.Visibility = Visibility.Hidden;
					OnScreenHint.Visibility = Visibility.Hidden;

					HideCursor();

					mainListBox.Focus();

					break;

				case ViewState.ShowingPercent:
					mainGrid.Visibility = Visibility.Hidden;
					PercentDisplay.Visibility = Visibility.Visible;
					OnScreenHint.Visibility = Visibility.Hidden;

					HideCursor();

					break;

				default:
					throw new ArgumentOutOfRangeException("newState");
			}
		}

		public void HideCursor()
		{
			Cursor = Cursors.None;
			MouseMove -= OnMouseMove;
			MouseMove += OnMouseMove;
			_currentMousePosition = Mouse.GetPosition(this);
		}

		void OnMouseMove(object sender, MouseEventArgs mouseEventArgs)
		{
			if ((mouseEventArgs.GetPosition(this) - _currentMousePosition).Length > 19)
			{
				Cursor = null;
				MouseMove -= OnMouseMove; // remove this handler for now
			}
		}

		public static ScrollViewer FindScrollViewer(DependencyObject listBox)
		{
			var scrollBorder = VisualTreeHelper.GetChild(listBox, 0) as Border;
			if (scrollBorder == null)
			{
				return null;
			}

			var scrollViewer = scrollBorder.Child as ScrollViewer;
			return scrollViewer;
		}

		#region Nested type: MoveDirection

		enum MoveDirection
		{
			NoChange = 0,
			PrevUp = -1,
			NextDown = 1
		}

		#endregion
	}
}